Skip to content

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Nov 1, 2025

This PR rewrites processRouteTree to yield a trie-like tree of segments. This replaces

  • packages/router-core/src/process-route-tree.ts
  • parts of packages/router-core/src/path.ts

Matching complexity now grows linearly with the number of segments (i.e. /a/b/c => 3 segments), as opposed to the previous algorithm that was correlated with the number of routes.

Goals:

  • faster pre-processing at start-time
  • faster matching at run-time
  • more consistent priority matching

Non-goals:

  • low RAM usage

Fixes #5635
Fixes #5674
Fixes #5799

to-do:

  • fix "off-by-1" (leading/trailing /)
  • implement sorting of same-type siblings at the node level (case sensitivity, prefix/suffix length)
  • implement single-match ("does A match B")
  • implement all-route-match ("which route does A match")
  • bench pre-processing against current implementation
  • bench single-match against current implementation
  • bench all-route-match against current implementation
  • fix not found page
  • fix URL param encoding (see failing tests)
  • write more tests and clean up existing tests
  • document priorities and "matching logic quirks"

Bench:

  • pre-processing
    • consistently >2x faster
  • needle-in-a-haystack route matching
    • for very small route trees: 3.5x faster
    • for very large route trees: 950x faster (430 routes)
    • non-matching: 90x faster (430 routes, random path "that almost matches" requested, this is a worst-case-scenario simulation)
  • 1-to-1 route matching (useMatchRoute)
    • for very simple routes: 1.5x slower
    • for very complex routes: 1x
  • resolvePath
    • 2.7x faster (added a LRU cache to this, it's called a lot and is easy to cache, but function itself could be improved)
  • interpolatePath
    • 1.4x faster

Notes:

  • in its current implementation, 1-to-1 route matching (useMatchRoute) does not care about the routeTree. It will match 2 arbitrary strings. This can lead to unexpected results, and is probably not the behavior we want in the long term.
    For now, this PR implements findSingleMatch to preserve that behavior, and this method is slower than the current implementation. However, when we switch to matching against the routeTree (using findRouteMatch instead), it will be faster.
  • We use an int32 bitmask to keep track of skipped optional segments, which means that segments beyond the 32nd segment cannot be optional
  • We use a UInt16Array to keep track of some indices, which also has a theoretical limit (65536 chars) but it is greater than the maximum length of URLs supported by most browsers (AFAIK, Chrome has the lowest limit, Firefox is exactly 65536, and Safari supports longer)
  • the segment tree can be built in several passes (like we do for route masks, provided as a list instead of a tree), so it is compatible with a "fog of war" approach. However, the tree must be walked to re-sort siblings after a batch of modifications.

Future improvements:

  • we could consider flattening the tree branches that contain a single child at each node
  • removing route-by-route case sensitivity configuration (keep global option) could yield some performance improvements
  • we could take inspiration from find-my-way (router for fastify) and use nodes that are smaller than a full pathname segment. This yields a structure close to a radix tree, which could perform better.

Summary by CodeRabbit

  • Deprecations

    • caseSensitive on match-route options is deprecated; use route- or router-level case sensitivity.
  • Breaking Changes

    • Legacy path parsing/matching public APIs removed; integrations must adopt the new route-tree pipeline.
    • DevTools explorer no longer exposes flatRoutes.
  • New Features

    • New processed route-tree matcher with revamped path resolution and interpolation for more accurate matching and params.
  • Tests

    • Added and updated tests covering the new route-tree matcher and parsing semantics.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 1, 2025

Walkthrough

Replaces the legacy pathname parser and route-tree processor with a new segment-trie processor, moves parsing/matching primitives into a new module, updates path utilities to use the segment model, replaces RouterCore's flatRoutes with processedTree and simplifies getMatchedRoutes, updates HMR and tests accordingly, and marks MatchRouteOptions.caseSensitive as deprecated.

Changes

Cohort / File(s) Summary
Documentation & Deprecation
docs/router/framework/react/api/router/MatchRouteOptionsType.md, packages/router-core/src/Matches.ts
caseSensitive?: boolean annotated as deprecated in docs and JSDoc; guidance added to configure case sensitivity at route or router level.
Removed Legacy Route Processor
packages/router-core/src/process-route-tree.ts
Entire file removed — previous route-tree building, scoring, sorting, flat route generation, and related exported types/functions deleted.
New Route Processor & Parser
packages/router-core/src/new-process-route-tree.ts
New segment-based parser/matcher added: parseSegment, segment node types/factories, trie construction, deterministic node sorting, matching primitives, param extraction, caching and public APIs (processRouteTree, processFlatRouteList, findRouteMatch, findFlatMatch, findSingleMatch, trimPathRight, etc.).
Path Utilities Refactor
packages/router-core/src/path.ts
Path utilities rewritten to use the new segment model: added joinPaths, cleanPath, trimPath*, removeTrailingSlash, exactPathTest, resolvePath; interpolatePath reimplemented; legacy parse/match exports and old Segment constants removed.
Router Core Migration
packages/router-core/src/router.ts
Replaced public flatRoutes with processedTree: ProcessedTree<...>; getMatchedRoutes simplified to accept only pathname and return foundRoute; internal calls migrated to new-process-route-tree APIs and caches updated.
Barrel & Framework Re-exports
packages/router-core/src/index.ts, packages/react-router/src/index.tsx, packages/solid-router/src/index.tsx
Removed re-exports of legacy path APIs/types: parsePathname, matchPathname, matchByPath, Segment; removed process-route-tree related exports from barrels.
Call Site & Usage Adjustments
packages/react-router/src/useBlocker.tsx, packages/solid-router/src/useBlocker.tsx, packages/start-server-core/src/createStartHandler.ts
Simplified router.getMatchedRoutes calls by dropping explicit second argument (now called with only pathname).
HMR & Plugin Updates
packages/router-plugin/src/core/route-hmr-statement.ts
Updated imports to include AnyRouter; replaced flatRoutes in-place swap with segment-tree walk/replace (walkReplaceSegmentTree), cleared caches, and updated processedTree nodes for HMR updates.
Devtools Surface
packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
Removed flatRoutes key from explorerState shaping; devtools explorer no longer exposes flatRoutes.
Tests: New & Updated
packages/router-core/tests/new-process-route-tree.test.ts, packages/router-core/tests/match-by-path.test.ts, packages/router-core/tests/optional-path-params*.test.ts, packages/router-core/tests/path.test.ts
New tests for the new processor added; many tests updated to import parsing/matching primitives from new-process-route-tree, to create processedTree via processRouteTree, and to align expectations with new parsing semantics (empty root pathname segment, unprefixed param names, etc.).
Framework Barrel Adjustments
packages/react-router/src/index.tsx, packages/solid-router/src/index.tsx
Dropped legacy parse/match exports and Segment type from framework barrels.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer
    participant RouterCore
    participant NewProc as new-process-route-tree
    participant PathUtil as path.ts

    rect rgb(230,245,234)
    Note over RouterCore,NewProc: Build processed route tree
    Dev->>RouterCore: buildRouteTree(routeTree)
    RouterCore->>NewProc: processRouteTree(routeTree, caseSensitive)
    NewProc->>NewProc: parseSegment / build trie
    NewProc-->>RouterCore: processedTree
    end

    rect rgb(245,245,230)
    Note over PathUtil,NewProc: Interpolation uses segment model
    Dev->>PathUtil: interpolatePath / resolvePath(...)
    PathUtil->>NewProc: parseSegment (traverse pattern)
    NewProc-->>PathUtil: segment node data
    PathUtil-->>Dev: final path
    end

    rect rgb(230,245,234)
    Note over RouterCore,NewProc: Runtime matching
    Dev->>RouterCore: getMatchedRoutes(pathname)
    RouterCore->>NewProc: findRouteMatch(pathname, processedTree)
    NewProc->>NewProc: trie traversal -> extractParams
    NewProc-->>RouterCore: matchedRoutes + foundRoute + params
    RouterCore-->>Dev: match result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • packages/router-core/src/new-process-route-tree.ts — parser/trie correctness, optional/wildcard/param edge cases, cache semantics.
    • packages/router-core/src/path.ts — interpolatePath and trailing-slash/encoding correctness.
    • packages/router-core/src/router.ts — migration from flatRoutes to processedTree and updated getMatchedRoutes usage.
    • Tests & barrels — ensure all removed exports are replaced and tests reflect new semantics.

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐇
I swapped old maps for branching trees,
I parse each hop with nimble knees.
Through segments, params, wild and neat,
I match your routes with tiny feet.
A carrot patch of tests — sweet treat!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main refactoring: replacing flatRoutes with a segment tree-based route processing system, which aligns with the substantial changes across router-core.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-process-route-tree-into-segment-tree

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Nov 1, 2025

View your CI Pipeline Execution ↗ for commit b6570e3

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 6m 52s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 23s View ↗

☁️ Nx Cloud last updated this comment at 2025-11-15 12:32:53 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 1, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5722

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5722

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5722

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5722

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5722

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5722

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5722

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5722

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5722

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5722

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5722

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5722

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5722

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5722

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5722

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5722

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5722

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5722

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5722

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5722

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5722

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5722

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5722

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@5722

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5722

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5722

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5722

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5722

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5722

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5722

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5722

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5722

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5722

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5722

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5722

commit: b6570e3

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/router-core/src/path.ts (1)

129-129: Unusual cache key separator—confirm this is intentional.

The cache key uses \\\\ as a separator between base and to paths. While this avoids conflicts with forward slashes, it's unconventional. If this choice was deliberate to prevent edge-case collisions, consider adding a brief inline comment explaining the rationale.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8df17ca and bf328f3.

📒 Files selected for processing (1)
  • packages/router-core/src/path.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/src/path.ts
📚 Learning: 2025-09-22T00:56:49.237Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.

Applied to files:

  • packages/router-core/src/path.ts
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/router-core/src/path.ts
📚 Learning: 2025-09-22T00:56:53.426Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.

Applied to files:

  • packages/router-core/src/path.ts
📚 Learning: 2025-10-01T18:31:35.420Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: e2e/react-start/custom-basepath/src/routeTree.gen.ts:58-61
Timestamp: 2025-10-01T18:31:35.420Z
Learning: Do not review files named `routeTree.gen.ts` in TanStack Router repositories, as these are autogenerated files that should not be manually modified.

Applied to files:

  • packages/router-core/src/path.ts
📚 Learning: 2025-09-28T21:41:45.233Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.

Applied to files:

  • packages/router-core/src/path.ts
🧬 Code graph analysis (1)
packages/router-core/src/path.ts (4)
packages/router-core/src/lru-cache.ts (1)
  • LRUCache (1-5)
packages/router-core/src/utils.ts (1)
  • last (191-193)
packages/router-core/src/new-process-route-tree.ts (6)
  • parseSegment (32-128)
  • SegmentKind (11-15)
  • SEGMENT_TYPE_PATHNAME (6-6)
  • SEGMENT_TYPE_PARAM (7-7)
  • SEGMENT_TYPE_WILDCARD (8-8)
  • SEGMENT_TYPE_OPTIONAL_PARAM (9-9)
packages/router-generator/src/utils.ts (1)
  • cleanPath (38-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (3)
packages/router-core/src/path.ts (3)

316-316: The 'undefined' string fallback is intentional and tested behavior.

Tests in packages/router-core/tests/path.test.ts explicitly verify that missing required parameters produce the literal string 'undefined' (lines 279–289). The interpolation also correctly sets isMissingParams = true to signal incomplete paths.

For comparison, optional parameters fall back to empty string, and splat parameters omit the segment entirely when missing. This design allows graceful degradation while flagging via isMissingParams that the path is incomplete.

The current behavior appears intentional and working as designed. However, verify whether this is the intended design for the PR or if a different approach (throwing errors or omitting segments) is preferred for required parameters.


117-207: Verify test status with actual test execution before merge.

Tests for URL param encoding exist in packages/router-core/tests/path.test.ts (including URI component encoding cases with encodeURIComponent and custom decodeCharMap handling). However, I cannot execute the test suite to confirm whether tests are currently passing or if any are actually failing.

Run the unit tests locally to verify the status of URL param encoding tests:

pnpm run test:unit

Ensure all path-related tests pass before merging, particularly the URI encoding tests in the interpolatePath describe block.


194-201: The reported serialization issue does not exist—code is functioning correctly.

Analysis of the code at lines 194-201 confirms:

  • prefix and suffix derive from substring() calls (lines 192-193), which always return strings, never undefined
  • The optional-param template literal ${prefix}{-$${value}}${suffix} produces exactly one $ character before the interpolated value: the {-$ contains a literal $ (not interpolation), followed by ${value} interpolation, creating {-$value} at runtime—not double-$ or "undefined" artifacts
  • Test coverage confirms correct behavior across all scenarios (100+ tests in optional-path-params.test.ts, curly-params-smoke.test.ts, and others show expected output without corruption)

No fixes needed.

Likely an incorrect or invalid review comment.

@Sheraff Sheraff marked this pull request as draft November 14, 2025 19:35
@Sheraff Sheraff marked this pull request as ready for review November 15, 2025 11:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/router-core/src/path.ts (1)

105-205: resolvePath drops trailing slash for wildcard + empty _splat when trailingSlash: 'always'

For a template like /splat/$ and trailingSlash: 'always', resolvePath currently:

  • Splits to ['', 'splat', '$', ''],
  • Appends '/' unconditionally when i > 0, even for the final empty segment,
  • Produces /splat//$, which cleanPath collapses to /splat/$.

Later, when _splat is missing/empty, interpolatePath removes the wildcard segment and returns /splat with no trailing /, contradicting the expectation that 'always' yields /splat/ (see the new test in path.test.ts).

You can fix this by making the join loop sensitive to leading/trailing empty segments instead of blindly inserting '/' for every index:

-  let segment
-  let joined = ''
-  for (let i = 0; i < baseSegments.length; i++) {
-    if (i > 0) joined += '/'
-    const part = baseSegments[i]!
-    if (!part) continue
-    segment = parseSegment(part, 0, segment)
-    const kind = segment[0]
-    if (kind === SEGMENT_TYPE_PATHNAME) {
-      joined += part
-      continue
-    }
-    const end = segment[5]
-    const prefix = part.substring(0, segment[1])
-    const suffix = part.substring(segment[4], end)
-    const value = part.substring(segment[2], segment[3])
-    if (kind === SEGMENT_TYPE_PARAM) {
-      joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`
-    } else if (kind === SEGMENT_TYPE_WILDCARD) {
-      joined += prefix || suffix ? `${prefix}{$}${suffix}` : '$'
-    } else {
-      // SEGMENT_TYPE_OPTIONAL_PARAM
-      joined += `${prefix}{-$${value}}${suffix}`
-    }
-  }
+  let segment
+  let joined = ''
+  for (let i = 0; i < baseSegments.length; i++) {
+    const part = baseSegments[i]!
+    const isFirst = i === 0
+    const isLast = i === baseSegments.length - 1
+
+    if (!part) {
+      // Leading empty segment -> ensure leading '/'
+      if (isFirst && !joined) {
+        joined = '/'
+      } else if (
+        isLast &&
+        trailingSlash === 'always' &&
+        !joined.endsWith('/')
+      ) {
+        // Trailing empty segment encodes the requested trailing slash
+        joined += '/'
+      }
+      continue
+    }
+
+    if (!isFirst && !joined.endsWith('/')) {
+      joined += '/'
+    }
+
+    segment = parseSegment(part, 0, segment)
+    const kind = segment[0]
+    if (kind === SEGMENT_TYPE_PATHNAME) {
+      joined += part
+      continue
+    }
+    const end = segment[5]
+    const prefix = part.substring(0, segment[1])
+    const suffix = part.substring(segment[4], end)
+    const value = part.substring(segment[2], segment[3])
+    if (kind === SEGMENT_TYPE_PARAM) {
+      joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`
+    } else if (kind === SEGMENT_TYPE_WILDCARD) {
+      joined += prefix || suffix ? `${prefix}{$}${suffix}` : '$'
+    } else {
+      // SEGMENT_TYPE_OPTIONAL_PARAM
+      joined += `${prefix}{-$${value}}${suffix}`
+    }
+  }

This preserves existing behavior for static/param segments, but for templates like /splat/$ it yields /splat/$/, and after interpolation with _splat === '' the final path becomes /splat/ as intended.

packages/router-core/src/router.ts (1)

2478-2533: matchRoute ignores router-level caseSensitive option

In the new implementation:

const match = findSingleMatch(
  next.pathname,
  opts?.caseSensitive ?? false,
  opts?.fuzzy ?? false,
  baseLocation.pathname,
  this.processedTree,
)

caseSensitive defaults to false whenever the caller doesn’t pass it, even if the router was configured with caseSensitive: true. Previously, route matching (and therefore matchRoute) respected the router-level caseSensitive default.

This can silently change behavior for apps that rely on case-sensitive routing but call matchRoute / useMatchRoute without explicitly specifying caseSensitive.

A minimal fix is to fall back to the router option:

-  const match = findSingleMatch(
-    next.pathname,
-    opts?.caseSensitive ?? false,
-    opts?.fuzzy ?? false,
-    baseLocation.pathname,
-    this.processedTree,
-  )
+  const match = findSingleMatch(
+    next.pathname,
+    opts?.caseSensitive ?? this.options.caseSensitive ?? false,
+    opts?.fuzzy ?? false,
+    baseLocation.pathname,
+    this.processedTree,
+  )

Everything else in matchRoute (deep-equality check for location.params, and optional search comparison) looks consistent with prior behavior.

♻️ Duplicate comments (1)
packages/router-core/src/new-process-route-tree.ts (1)

353-372: Add stable tie-breaker for deterministic route ordering.

The sortDynamic function returns 0 when two segments have identical prefix, suffix, and case-sensitivity properties. This creates non-deterministic ordering when multiple routes share these attributes, which can cause inconsistent matching behavior across runs or JavaScript engines.

Add a final alphabetical comparison by fullPath to ensure stable, deterministic ordering:

  if (a.suffix && !b.suffix) return -1
  if (!a.suffix && b.suffix) return 1
  if (a.caseSensitive && !b.caseSensitive) return -1
  if (!a.caseSensitive && b.caseSensitive) return 1
+ // Stable tie-breaker for deterministic ordering
+ return a.fullPath.localeCompare(b.fullPath)
- return 0
}
🧹 Nitpick comments (2)
packages/router-core/tests/optional-path-params-clean.test.ts (1)

15-51: Shared parsePathname/PathSegment helpers could be centralized

This PathSegment + parsePathname combo is effectively identical to the one in optional-path-params.test.ts. Consider extracting a tiny test utility (e.g. test-utils/segment-helpers.ts) to keep these helpers in sync and make future parser changes easier to roll out across tests.

packages/router-core/src/new-process-route-tree.ts (1)

510-522: Remove commented code before final merge.

The commented-out intoRouteLike function should be removed if it's no longer needed, or uncommented and documented if it will be used.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf328f3 and f952c87.

📒 Files selected for processing (7)
  • packages/router-core/src/new-process-route-tree.ts (1 hunks)
  • packages/router-core/src/path.ts (4 hunks)
  • packages/router-core/src/router.ts (10 hunks)
  • packages/router-core/tests/new-process-route-tree.test.ts (1 hunks)
  • packages/router-core/tests/optional-path-params-clean.test.ts (2 hunks)
  • packages/router-core/tests/optional-path-params.test.ts (5 hunks)
  • packages/router-core/tests/path.test.ts (18 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-core/tests/new-process-route-tree.test.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/tests/path.test.ts
  • packages/router-core/src/path.ts
  • packages/router-core/tests/optional-path-params.test.ts
  • packages/router-core/tests/optional-path-params-clean.test.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/router.ts
📚 Learning: 2025-09-22T00:56:49.237Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.

Applied to files:

  • packages/router-core/tests/path.test.ts
  • packages/router-core/src/path.ts
  • packages/router-core/src/new-process-route-tree.ts
📚 Learning: 2025-10-14T18:59:33.990Z
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.

Applied to files:

  • packages/router-core/tests/path.test.ts
  • packages/router-core/tests/optional-path-params.test.ts
  • packages/router-core/tests/optional-path-params-clean.test.ts
📚 Learning: 2025-09-28T21:41:45.233Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5284
File: e2e/react-start/basic/server.js:50-0
Timestamp: 2025-09-28T21:41:45.233Z
Learning: In Express v5, catch-all routes must use named wildcards. Use `/*splat` to match everything except root path, or `/{*splat}` (with braces) to match including root path. The old `*` syntax is not allowed and will cause "Missing parameter name" errors. This breaking change requires explicit naming of wildcard parameters.

Applied to files:

  • packages/router-core/tests/path.test.ts
  • packages/router-core/src/path.ts
  • packages/router-core/tests/optional-path-params.test.ts
  • packages/router-core/tests/optional-path-params-clean.test.ts
  • packages/router-core/src/new-process-route-tree.ts
📚 Learning: 2025-09-22T00:56:53.426Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.

Applied to files:

  • packages/router-core/tests/path.test.ts
  • packages/router-core/src/path.ts
  • packages/router-core/src/new-process-route-tree.ts
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/router-core/src/path.ts
  • packages/router-core/tests/optional-path-params-clean.test.ts
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/router.ts
📚 Learning: 2025-10-01T18:31:35.420Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: e2e/react-start/custom-basepath/src/routeTree.gen.ts:58-61
Timestamp: 2025-10-01T18:31:35.420Z
Learning: Do not review files named `routeTree.gen.ts` in TanStack Router repositories, as these are autogenerated files that should not be manually modified.

Applied to files:

  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/src/router.ts
🧬 Code graph analysis (6)
packages/router-core/tests/path.test.ts (2)
packages/router-core/src/path.ts (2)
  • interpolatePath (247-349)
  • resolvePath (116-206)
packages/router-core/src/new-process-route-tree.ts (7)
  • processRouteTree (645-702)
  • findSingleMatch (595-615)
  • SegmentKind (11-15)
  • parseSegment (54-151)
  • SEGMENT_TYPE_PATHNAME (6-6)
  • SEGMENT_TYPE_PARAM (7-7)
  • SEGMENT_TYPE_WILDCARD (8-8)
packages/router-core/src/path.ts (4)
packages/router-core/src/lru-cache.ts (1)
  • LRUCache (1-5)
packages/router-core/src/utils.ts (1)
  • last (191-193)
packages/router-core/src/new-process-route-tree.ts (5)
  • parseSegment (54-151)
  • SEGMENT_TYPE_PATHNAME (6-6)
  • SEGMENT_TYPE_PARAM (7-7)
  • SEGMENT_TYPE_WILDCARD (8-8)
  • SEGMENT_TYPE_OPTIONAL_PARAM (9-9)
packages/router-generator/src/utils.ts (1)
  • cleanPath (38-41)
packages/router-core/tests/optional-path-params.test.ts (1)
packages/router-core/src/new-process-route-tree.ts (8)
  • SegmentKind (11-15)
  • parseSegment (54-151)
  • SEGMENT_TYPE_PATHNAME (6-6)
  • SEGMENT_TYPE_OPTIONAL_PARAM (9-9)
  • SEGMENT_TYPE_PARAM (7-7)
  • SEGMENT_TYPE_WILDCARD (8-8)
  • processRouteTree (645-702)
  • findSingleMatch (595-615)
packages/router-core/tests/optional-path-params-clean.test.ts (1)
packages/router-core/src/new-process-route-tree.ts (6)
  • SegmentKind (11-15)
  • parseSegment (54-151)
  • SEGMENT_TYPE_PATHNAME (6-6)
  • SEGMENT_TYPE_OPTIONAL_PARAM (9-9)
  • processRouteTree (645-702)
  • findSingleMatch (595-615)
packages/router-core/src/new-process-route-tree.ts (2)
packages/router-core/src/route.ts (3)
  • path (1551-1553)
  • RouteLike (1968-1979)
  • fullPath (1555-1557)
packages/router-core/src/lru-cache.ts (2)
  • LRUCache (1-5)
  • createLRUCache (7-74)
packages/router-core/src/router.ts (5)
packages/router-core/src/new-process-route-tree.ts (7)
  • ProcessedTree (539-554)
  • processRouteTree (645-702)
  • processRouteMasks (556-573)
  • findFlatMatch (578-590)
  • findSingleMatch (595-615)
  • trimPathRight (637-639)
  • findRouteMatch (617-634)
packages/router-core/src/lru-cache.ts (1)
  • createLRUCache (7-74)
packages/router-core/src/route.ts (2)
  • path (1551-1553)
  • RouteMask (1466-1475)
packages/router-core/src/path.ts (3)
  • resolvePath (116-206)
  • cleanPath (25-28)
  • trimPathRight (38-40)
packages/router-core/src/utils.ts (1)
  • deepEqual (333-387)
🔇 Additional comments (15)
packages/router-core/tests/optional-path-params.test.ts (2)

15-55: Local parsePathname correctly tracks parseSegment output

The helper cleanly mirrors parseSegment’s index contract (type/value/prefix/suffix) and reuses the buffer, so these tests are exercising the new segment parser accurately without extra allocations. Looks good.


334-354: matchPathname wrapper preserves legacy matchPathname behavior

Wiring findSingleMatch(options.to, …, from, processedTree) is consistent with the new function’s (from, …, path) signature and the legacy { to }-based API. Returning undefined only when to is provided and no match is found also matches previous semantics.

packages/router-core/tests/optional-path-params-clean.test.ts (1)

141-161: matchPathname test harness is consistent with the new single-route matcher

The helper correctly seeds a minimal processedTree and delegates matching to findSingleMatch, so these tests validate the new matcher rather than the old path helpers. No issues spotted.

packages/router-core/tests/path.test.ts (1)

362-395: Trailing-slash interpolation tests align with interpolatePath behavior

These new cases around required vs optional params and trailing / give good coverage of the updated interpolation logic, including optional segments with and without values. They match the current implementation’s intent.

packages/router-core/src/path.ts (1)

221-347: interpolatePath + encodeParam logic looks sound

The new cursor-based interpolation using parseSegment correctly distinguishes pathname, param, optional param, and wildcard segments, and:

  • Tracks usedParams and isMissingParams consistently (including the _splat missing cases the tests assert on),
  • Leaves / unencoded for _splat via encodeURI, while routing all other params through encodePathParam + decodeCharMap,
  • Handles optional params with prefix/suffix by omitting just the param when the value is null/undefined.

Given the existing tests around numeric params, URI encoding, missing params, and optional segments, the behavior here matches expectations.

packages/router-core/src/router.ts (4)

1212-1221: resolvePathWithBase cache wiring is straightforward

Using a dedicated resolvePathCache LRU and passing it into resolvePath is a nice way to avoid recomputing path resolutions. The cache key intentionally omits trailingSlash based on the assumption that a router instance uses a fixed trailingSlash option, which matches how this cache is used here.


1453-1551: getMatchedRoutes integration with ProcessedTree looks correct

RouterCore.getMatchedRoutes now delegates to the exported getMatchedRoutes helper with { pathname, routesById, processedTree }. The helper uses findRouteMatch(trimPathRight(pathname), processedTree, true) and falls back to the root route when no match is found, preserving the previous fuzzy-root semantics. Param propagation into routeParams via Object.assign also guards against mutating cached match objects.


1782-1797: Route mask matching via findFlatMatch is well-structured

Using findFlatMatch<RouteMask<TRouteTree>>(next.pathname, this.processedTree) and copying match.params into a fresh params object avoids mutating cached matches, and destructuring { from: _from, ...maskProps } to build maskedDest keeps the public from field out of the masked navigation options. Behavior-wise this aligns with the existing route-masking model.


2626-2655: getMatchedRoutes helper correctly switches to findRouteMatch

The exported getMatchedRoutes now uses findRouteMatch(trimPathRight(pathname), processedTree, true) instead of flat-route parsing, then walks parentRoute links to build matchedRoutes, defaulting to the root route when no match is found. This is a clean adaptation to the new ProcessedTree representation and matches the router’s existing fuzzy-not-found behavior.

packages/router-core/src/new-process-route-tree.ts (6)

1-151: Well-structured parsing foundation.

The segment parsing logic is clean and efficient. The use of Uint16Array for parsed segment representation is a smart performance optimization, and the regex patterns correctly handle the braced parameter syntax. The parseSegment function properly handles all segment types with appropriate index boundary tracking.


162-351: Solid recursive tree-building logic.

The parseSegments function correctly handles tree construction with proper depth tracking, node reuse, and case sensitivity. The logic to distinguish between layout routes and index routes (lines 329-337) is well thought out, and the recursive child processing maintains correct parent-child relationships.


405-455: Well-structured node creation with performance considerations.

The node creation functions properly initialize all fields, and the comment about field ordering for engine optimization shows attention to performance details. The type structure using discriminated unions is clean and type-safe.


556-702: Comprehensive tree processing with proper caching.

The tree processing functions correctly handle both tree-based and flat route structures. The LRU caching strategy is well-applied, and the depth initialization is consistent throughout. The routesByPath logic that prioritizes index routes (line 679) is correct for handling ambiguous cases.


724-801: Parameter extraction logic is well-structured.

The extractParams and buildBranch functions correctly handle all segment types with proper decoding. The lazy initialization of nodeParts and the bitmask optimization for optional params show good attention to performance.

The URL encoding complexities (matching encoded path parts against unencoded route prefixes/suffixes) are inherently challenging and likely the source of the failing tests mentioned in the PR objectives.


803-1055: Sophisticated matching algorithm with correct priority handling.

The stack-based matching approach efficiently explores all possible routes with proper priority ordering. The use of statics/dynamics/optionals counters to track match quality is elegant, and the bitmask optimization for skipped optional params is clever. The wildcard suffix check (line 919) correctly handles multi-segment remaining paths by joining and checking the trailing characters.

This is a complex but well-designed algorithm that correctly implements the intended matching semantics.

@Sheraff Sheraff merged commit f39eb9b into main Nov 15, 2025
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-process-route-tree-into-segment-tree branch November 15, 2025 12:45
@Sheraff Sheraff changed the title refactor(router-core): Process routeTree into segment tree instead of flatRoutes [WIP] refactor(router-core): Process routeTree into segment tree instead of flatRoutes Nov 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment